<?php
/**
 * dbfront
 * (c) 2010-2013 Stephen Adkins <spadkins@gmail.com>
 * This software began development in 2010.
 * It is based on code dating from 1999.
 *
 * License: GPL 2.0
 * The contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL").
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 * */

#############################################################################
# Variables
#############################################################################

global $calldepth, $scope, $scope_exclusive, $trace, $trace_width, $trace_justify, $debug, $debug_sql, $debug_file, $debug_fh;
$calldepth         = 0;
$scope             = null;
$scope_exclusive   = 0;
$debug             = 0;
$debug_options     = 0;
$debug_sql         = 0;
$trace             = 0;
$trace_width       = 190;
$trace_justify     = 1;
$debug_file        = "";
$debug_fh          = null;

global $App_options, $App_prefix;
$App_options = null;
$App_prefix  = dirname(dirname(dirname(__FILE__)));

function app_autoloader($class_name) {
    $class_file = preg_replace('/\\\\/','/',$class_name) . ".php";
    $class_file = preg_replace('/_/','/',$class_file);
    #error_log("app_autoloader($class_name) => [$class_file]\n");
    include_once $class_file;
}
spl_autoload_register('app_autoloader');

class App {
    private $context;
    static public function context (&$options = null) {
        global $scope, $scope_exclusive, $trace, $trace_width, $trace_justify, $debug, $debug_options, $debug_sql, $debug_file, $App_options, $App_prefix;
        if (!isset($context)) {
            if (!isset($options)) $options = array();
            if (isset($options['prefix'])) $App_prefix  = $options['prefix'];
            if (isset($options['debug']))         $debug         = $options['debug'];
            if (isset($options['debug_options'])) $debug_options = $options['debug_options'];
            if (isset($options['debug_sql']))     $debug_sql     = $options['debug_sql'];
            if (isset($options['trace']))         $trace         = $options['trace'];
            if (isset($options['trace_width']))   $trace_width   = $options['trace_width'];
            if (isset($options['trace_justify'])) $trace_justify = $options['trace_justify'];
            if (isset($options['debug_file']))    $debug_file    = $options['debug_file'];

            #if (!defined('STDIN'))  { define('STDIN',  fopen('php://stdin',  'r')); }
            #if (!defined('STDOUT')) { define('STDOUT', fopen('php://stdout', 'w')); }
            #if (!defined('STDERR')) { define('STDERR', fopen('php://stderr', 'w')); }

            if ($debug || $trace || $debug_options || $debug_sql) debug_open();

            if ($debug) {
                debug_print("##############################################################################\n");
                if (isset($_SERVER['SERVER_NAME']) && isset($_SERVER['SCRIPT_NAME'])) {
                    $url = 'http://' . $_SERVER['SERVER_NAME'] . $_SERVER['SCRIPT_NAME'];
                    debug_print("# URL: $url\n");
                    debug_print("##############################################################################\n");
                }
                debug_print('$_SERVER');
                debug_print(json_encode($_SERVER,JSON_PRETTY_PRINT)."\n");
                debug_print('$_REQUEST');
                debug_print(json_encode($_REQUEST,JSON_PRETTY_PRINT)."\n");
                debug_print('REQUEST-HEADERS');
                debug_print(json_encode(\App::getHeaders(),JSON_PRETTY_PRINT)."\n");
            }
            if ($debug_options) {
                debug_print("##############################################################################\n");
                debug_print("# OPTIONS: debug_options=[$debug_options], options_file=[$options[options_file]]\n");
                debug_print("##############################################################################\n");
                debug_print(json_encode($options,JSON_PRETTY_PRINT)."\n");
            }

            $context_class = isset($options['app.context.class']) ? $options['app.context.class'] : "App\\Context";
            $context = new $context_class($options);

            $App_options = $options;
        }
        return($context);
    }

    private function getHeaders() {
        #[SERVER_SOFTWARE] => Apache/2.4.6 (Ubuntu)
        # $server_software = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : 'unknown';
        # if (function_exists('apache_request_headers') && preg_match('/apache/i', $server_software)) { }
        if (function_exists('apache_request_headers')) {
            return apache_request_headers();
        }
        $headers = array();
        $keys = preg_grep('{^HTTP_}i', array_keys($_SERVER));
        foreach ($keys as $val) {
            $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($val, 5)))));
            $headers[$key] = $_SERVER[$val];
        }
        return $headers;
    }
    function __destruct () {
        global $debug_fh;
        if ($debug_fh) fclose($debug_fh);
    }
}

#############################################################################
# trace_entry()
#############################################################################

function trace_entry () {
    global $calldepth, $scope, $scope_exclusive, $trace, $trace_width, $trace_justify, $debug, $debug_sql, $debug_file, $debug_fh, $App_prefix;
    if ($trace) {
        $stacklevel = 1;
        $backtrace = debug_backtrace();
        if (count($backtrace) >= 2) {
            $calling_frame = $backtrace[1];

            # $calling_frame['function']  string   The current function name. See also __FUNCTION__.
            # $calling_frame['line']      integer  The current line number. See also __LINE__.
            # $calling_frame['file']      string   The current file name. See also __FILE__.
            # $calling_frame['class']     string   The current class name. See also __CLASS__
            # $calling_frame['object']    object   The current object.
            # $calling_frame['type']      string   The current call type. If a method call, "->" is returned. If a static method call, "::" is returned. If a function call, nothing is returned.
            # $calling_frame['args']      array    If inside a function, this lists the functions arguments. If inside an included file, this lists the included file name(s).

            $function  = (isset($calling_frame['function'])) ? str_replace('App\\','',$calling_frame['function']) : "";
            $call_line = (isset($calling_frame['line']))     ? $calling_frame['line']     : 0;
            $call_file = (isset($calling_frame['file']))     ? str_replace("$App_prefix/",'',$calling_frame['file']) : "";
            if ($call_file)                                    $call_file = preg_replace('!.*/apps/!','',$call_file);
            $class     = (isset($calling_frame['class']))    ? str_replace('App\\','',$calling_frame['class'])   : "";
            $object    = (isset($calling_frame['object']))   ? $calling_frame['object']   : null;
            $type      = (isset($calling_frame['type']))     ? $calling_frame['type']     : "";
            $args      = (isset($calling_frame['args']))     ? $calling_frame['args']     : null;

            if ($scope) {
                if ($scope_exclusive) {
                    if (isset($scope[$class]) || isset($scope["$class.$function"])) return;
                }
                else {
                    if (!isset($scope[$class]) && !isset($scope["$class.$function"])) return;
                }
            }

            #echo "XXX: [$function] [$call_line] [$call_file] [$class] [$object] [$type] [$args]\n";
            if ($object) {  # dynamic method, called on an object
                $object_class = str_replace('App\\','',get_class($object));
                #echo "XXX: {$object_class}($object->name)->$function($args) : (defined in: $class, called from: {$call_file}[$call_line])\n";
                $object_name = (is_a($object, 'App\\Service')) ? "{$object_class}($object->name)" : $object_class;
                $text = str_repeat('| ', $calldepth) . "+-" . "$object_name$type$function(";
                $trailer = " [defined in: $class] [called from: {$call_file}($call_line)]";
            }
            elseif ($class) {   # static method, called on a class
                $text = str_repeat('| ', $calldepth) . "+-" . "$class$type$function(";
                $trailer = " [defined in: $class] [called from: {$call_file}($call_line)]";
            }
            else {
                $text = str_repeat('| ', $calldepth) . "+-" . $function . "(";
                $trailer = "";
            }

            if (count($args) > 0) {
                foreach ($args as $narg => $arg) {
                    if ($narg) $text .= ",";
                    if (!isset($arg)) {
                        $text .= "null";
                    }
                    elseif (is_object($arg)) {
                        $text .= get_class($arg);
                    }
                    elseif (!is_array($arg)) {
                        try {
                            $text .= $arg;
                        }
                        catch (Exception $e) {
                            $text .= "???";
                        }
                    }
                    elseif (is_assoc($arg)) {
                        $text .= '[';
                        $first = 1;
                        foreach ($arg as $key => $value) {
                            if (!$first) $text .= ',';
                            $text .= is_array($value) ? "$key=>[Array]" : "$key=>$value";
                            $first = 0;
                        }
                        $text .= ']';
                    }
                    else {
                        $text .= '[';
                        foreach ($arg as $i => $item) {
                            if ($i > 0) $text .= ',';
                            if     (!isset($item))    $text .= 'null';
                            elseif (is_object($item)) $text .= '{' . get_class($item) . '}';
                            elseif (!is_array($item)) {
                                try {
                                    $text .= $item;
                                }
                                catch (Exception $e) {
                                    $text .= "???";
                                }
                            }
                            elseif (is_assoc($item))  $text .= '{...}';
                            else                      $text .= '[...]';
                        }
                        $text .= ']';
                    }
                }
            }
            else {
                $text .= '';
            }

            #$trailer .= " [class=$class function=$function class=$class method=$method]";
            $text .= ")";
            $trailer_len = strlen($trailer);
            $text = preg_replace("/\\n/",'\\n', $text);
            $text_len = strlen($text);
            if ($trace_width) {
                if ($text_len + $trailer_len > $trace_width) {
                    $len = $trace_width - $trailer_len;
                    if ($len < 1) $len = 1;
                    fwrite($debug_fh, substr($text, 0, $len) . $trailer . "\n");
                }
                elseif ($trace_justify) {
                    $len = $trace_width - $trailer_len - $text_len;
                    if ($len < 0) $len = 0;  # should never happen
                    fwrite($debug_fh, $text . str_repeat('.', $len) . $trailer . "\n");
                }
                else {
                    fwrite($debug_fh, $text . $trailer . "\n");
                }
            }
            else {
                fwrite($debug_fh, $text . $trailer . "\n");
            }

            $calldepth++;
        }
    }
}

#############################################################################
# trace_exit()
#############################################################################

function trace_exit () {
    global $calldepth, $scope, $scope_exclusive, $trace, $trace_width, $trace_justify, $debug, $debug_sql, $debug_file, $debug_fh, $App_prefix;
    if ($trace) {
        $stacklevel = 1;

        $backtrace = debug_backtrace();
        $calling_frame = $backtrace[1];
        $function  = (isset($calling_frame['function'])) ? str_replace('App\\','',$calling_frame['function']) : "";
        $call_line = (isset($calling_frame['line']))     ? $calling_frame['line']     : 0;
        $call_file = (isset($calling_frame['file']))     ? str_replace("$App_prefix/",'',$calling_frame['file']) : "";
        $class     = (isset($calling_frame['class']))    ? str_replace('App\\','',$calling_frame['class'])   : "";
        $object    = (isset($calling_frame['object']))   ? str_replace('App\\','',get_class($calling_frame['object']))  : null;
        $type      = (isset($calling_frame['type']))     ? $calling_frame['type']    : "";
        $args      = func_get_args();

        #foreach ($backtrace as $btrace) {
        #    printf("Backtrace: %-20.20s %6d %-70.70s %-20.20s %-12.12s [%s]\n",
        #        (isset($btrace['function'])) ? $btrace['function'] : "",
        #        (isset($btrace['line']))     ? $btrace['line']     : 0,
        #        (isset($btrace['file']))     ? str_replace("$App_prefix/",'',$btrace['file']) : "",
        #        (isset($btrace['class']))    ? str_replace('App\\','',$btrace['class']) : "",
        #        (isset($btrace['type']))     ? $btrace['type']     : "",
        #        (isset($btrace['object']))   ? str_replace('App\\','',get_class($btrace['object'])) : "");
        #}

        if ($scope) {
            if ($scope_exclusive) {
                if ($scope{$class} || $scope{"$class.$function"}) return;
            }
            else {
                if (!$scope{$class} && !$scope{"$class.$function"}) return;
            }
        }

        $calldepth--;
        $text = str_repeat('| ', $calldepth) . "+-> return(";
        if (count($args) > 0) {
            foreach ($args as $narg => $arg) {
                $text .= $narg ? "," : "";
                if (!isset($arg)) {
                    $text .= "null";
                }
                elseif (is_object($arg)) {
                    $text .= get_class($arg);
                }
                elseif (!is_array($arg)) {
                    try {
                        $text .= $arg;
                    }
                    catch (Exception $e) {
                        $text .= "???";
                    }
                }
                elseif (is_assoc($arg)) {
                    $text .= '[';
                    $first = 1;
                    foreach ($arg as $key => $value) {
                        if (!$first) $text .= ',';
                        $text .= is_array($value) ? "$key=>[Array]" : "$key=>$value";
                        $first = 0;
                    }
                    $text .= ']';
                }
                else {
                    $text .= '[';
                    foreach ($arg as $i => $item) {
                        if ($i > 0) $text .= ',';
                        if     (!isset($item))   $text .= 'null';
                        elseif (is_object($arg)) $text .= '{' . get_class($arg) . '}';
                        elseif (!is_array($arg)) $text .= '???';
                        elseif (is_assoc($arg))  $text .= '{...}';
                        else                     $text .= '[...]';
                    }
                    $text .= ']';
                }
            }
            $text .= ")";
        }
        else {
            $text .= ")";
        }
        $text = preg_replace("/\n/",'\\n', $text);
        if ($trace_width && strlen($text) > $trace_width) {
            fwrite($debug_fh, substr($text, 0, $trace_width) . "\n");
        }
        else {
            fwrite($debug_fh, $text . "\n");
        }
    }
}

#############################################################################
# debug_open()
#############################################################################

function debug_open () {
    global $options, $debug_file, $debug_fh;
    if     (!isset($debug_file))     { $debug_fh = fopen('php://stderr', 'w'); }
    elseif ($debug_file == 'STDERR') { $debug_fh = fopen('php://stderr', 'w'); }
    elseif ($debug_file == 'STDOUT') { $debug_fh = fopen('php://stdout', 'w'); }
    else {
        $filemode = isset($options['debug_file_mode']) ? $options['debug_file_mode'] : 'w';
        $debug_fh = fopen($debug_file,$filemode);
    }
    if (!$debug_fh) throw new \Exception("debug_file cannot be opened [fopen($debug_file,$filemode)]");
}

#############################################################################
# debug_print()
#############################################################################

function debug_print ($text = null) {
    global $options, $calldepth, $scope, $scope_exclusive, $trace, $trace_width, $trace_justify, $debug, $debug_sql, $debug_file, $debug_fh, $App_prefix;
    if ($debug) {
        if (!$debug_fh) debug_open();
        if ($scope) {
            if ($scope_exclusive) {
                if ($scope{$class} || $scope{"$class.$function"}) return;
            }
            else {
                if (!$scope{$class} && !$scope{"$class.$function"}) return;
            }
        }
        $indent_str = $calldepth ? str_repeat('| ', $calldepth) . '* ' : '';
        if (!isset($text)) {
            $backtrace = debug_backtrace();
            if (count($backtrace) >= 1) {
                $calling_frame = $backtrace[0];
                $call_file = (isset($calling_frame['file']))     ? str_replace("$App_prefix/",'',$calling_frame['file']) : "";
                if ($call_file)                                    $call_file = preg_replace('!.*/apps/!','',$call_file);
                $class     = (isset($calling_frame['class']))    ? $calling_frame['class']    : $call_file;
                $call_line = (isset($calling_frame['line']))     ? $calling_frame['line']     : 0;
                $text = "$class($call_line)";
            }
        }
        $strs = explode("\n", $text);
        foreach ($strs as $str) {
            if (strlen($str) > 0) {
                $str = $indent_str . $str;
                if ($trace_width && strlen($str) > $trace_width) {
                    fwrite($debug_fh, substr($str, 0, $trace_width) . "\n");
                }
                else {
                    fwrite($debug_fh, $str . "\n");
                }
            }
        }
    }
}

#############################################################################
# in_debug_scope()
#############################################################################

function in_debug_scope () {
    global $calldepth, $scope, $scope_exclusive, $trace, $trace_width, $trace_justify, $debug, $debug_sql, $debug_file, $debug_fh, $App_prefix;
    if ($debug) {
        $stacklevel = 1;

        $backtrace = debug_backtrace();
        $calling_frame = $backtrace[1];
        $function  = (isset($calling_frame['function'])) ? str_replace('App\\','',$calling_frame['function']) : "";
        $call_line = (isset($calling_frame['line']))     ? $calling_frame['line']     : 0;
        $call_file = (isset($calling_frame['file']))     ? str_replace("$App_prefix/",'',$calling_frame['file']) : "";
        $class     = (isset($calling_frame['class']))    ? str_replace('App\\','',$calling_frame['class'])   : "";
        $object    = (isset($calling_frame['object']))   ? str_replace('App\\','',get_class($calling_frame['object']))  : null;
        $type      = (isset($calling_frame['type']))     ? $calling_frame['type']    : "";
        $args      = (isset($calling_frame['args']))     ? $calling_frame['args']     : null;

        if ($scope) {
            if ($scope_exclusive) {
                 if ($scope{$class} || $scope{"$class.$function"}) return(0);
            }
            else {
                if (!$scope{$class} && !$scope{"$class.$function"}) return(0);
            }
        }
        return(1);
    }
    return(0);
}

#############################################################################
# debug_indent()
#############################################################################

function debug_indent () {
    global $calldepth, $scope, $scope_exclusive, $trace, $trace_width, $trace_justify, $debug, $debug_sql, $debug_file, $debug_fh, $App_prefix;
    $text = str_repeat('| ', $calldepth) . '  * ';
    return($text);
}

function is_assoc (&$array) {
    if (!array_key_exists(0, $array)) return 1;
    $count = count($array);
    if (!array_key_exists($count-1, $array)) return 1;
    return 0;
}

